﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Diagnostics;

namespace dbgptc
{
    class DbgpTunnel
    {
        public const byte STATUS_OK = 0;
        public const byte STATUS_IDEKEY_IN_USE = 3;
        public const byte STATUS_DISCONNECTED = 255;

        protected Socket _tunnel;
        private TunnelProtocol _tunnelProtocol;
        private EventHandler<SocketAsyncEventArgs> _tunnelReceiveCallback;

        private string _sessionAddr;
        private int _sessionPort;
        protected Socket _session;
        private DbgpProtocol _dbgpProtocol;
        private EventHandler<SocketAsyncEventArgs> _sessionReceiveCallback;

        private byte[] _statusBuffer = new byte[1];

        public DbgpTunnel(string tunnel_addr, int tunnel_port, string idekey)
        {
            _tunnelReceiveCallback = new EventHandler<SocketAsyncEventArgs>(messageReceived);
            _sessionReceiveCallback = new EventHandler<SocketAsyncEventArgs>(commandReceived);
            
            setupTunnelProtocolHandler();
            setupDbgpProtocolHandler();

            _tunnel = connectSocket(tunnel_addr, tunnel_port);
            if (_tunnel == null) {
                throw new Exception("failed to connect to the listner server");
            }

            setIdeKey(idekey);
        }

        private void setIdeKey(string idekey)
        {
            _statusBuffer[0] = STATUS_OK;
            _tunnel.Send(_statusBuffer);
            _tunnel.Send(Encoding.ASCII.GetBytes(idekey + '\0'));
            _tunnel.Receive(_statusBuffer, 1, SocketFlags.None);
            switch (_statusBuffer[0]) {
                case STATUS_OK:
                    return;
                case STATUS_IDEKEY_IN_USE:
                    throw new Exception("IDEKEY already is in use");
                default:
                    throw new Exception(string.Format("the tunnel server returns unknown connection status {0}", _statusBuffer[0]));
            }
        }

        public void run(string session_addr, int session_port)
        {
            _sessionAddr = session_addr;
            _sessionPort = session_port;
            beginReceiveMessage();
        }

        private void shutdownTunnel()
        {
            if (_session != null) {
                Debug.Print("ShutdownTunnel()");
                _session.Shutdown(SocketShutdown.Both);
                _session = null;
            }
        }

        private void shutdownDbgpSession()
        {
            if (_tunnel != null) {
                Debug.Print("shutdownDbgpSession()");
                _tunnel.Shutdown(SocketShutdown.Both);
                _tunnel = null;
            }
        }

        public void stop()
        {
            shutdownDbgpSession();
            shutdownTunnel();
        }

        private void beginReceiveMessage()
        {
            byte[] buffer = new byte[4096];

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.SetBuffer(buffer, 0, buffer.Length);
            args.Completed += _tunnelReceiveCallback;
            bool async = _tunnel.ReceiveAsync(args);
            if (!async) {
                this.messageReceived(this, args);
            }
        }

        private void beginReceiveCommand()
        {
            byte[] buffer = new byte[4096];

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.SetBuffer(buffer, 0, buffer.Length);
            args.Completed += _sessionReceiveCallback;
            bool async = _session.ReceiveAsync(args);
            if (!async) {
                this.commandReceived(this, args);
            }
        }

        private void setupTunnelProtocolHandler()
        {
            _tunnelProtocol = new TunnelProtocol();

            _tunnelProtocol.onErrorStatus += delegate(object protocol, byte status) {
                if (_session != null) {
                    shutdownDbgpSession();
                    return;
                }
            };

            _tunnelProtocol.onMessage += delegate(object protocol, byte[] buffer, int offset, int length, SocketAsyncEventArgs args) {
                Debug.Print("message: {0}", Encoding.ASCII.GetString(buffer, offset, length));
                if (isDbgpSessionActive()) {
                    _session.Send(buffer, offset, length, SocketFlags.None);
                }
            };
        }

        private void setupDbgpProtocolHandler()
        {
            _dbgpProtocol = new DbgpProtocol();

            _dbgpProtocol.onCommandStart += delegate(object protocol) {
                _statusBuffer[0] = STATUS_OK;
                _tunnel.Send(_statusBuffer, SocketFlags.None);
            };

            _dbgpProtocol.onCommand += delegate(object protocol, byte[] buffer, int offset, int length, SocketAsyncEventArgs args) {
                _tunnel.Send(buffer, offset, length, SocketFlags.None);
            };
        }

        private bool isTunnelActive()
        {
            return (_tunnel != null && _tunnel.Connected);
        }

        private bool isDbgpSessionActive()
        {
            return (_session != null && _session.Connected);
        }

        private void messageReceived(object sender, SocketAsyncEventArgs args)
        {
            if (!isDbgpSessionActive()) {
                _session = connectSocket(_sessionAddr, _sessionPort);
                if (_session == null) {
                    shutdownTunnel();
                    return;
                }
                beginReceiveCommand();
            }

            while (isTunnelActive() && isDbgpSessionActive()) {
                _tunnelProtocol.processMessage(args);
                if (!isTunnelActive()) {
                    break;
                }

                bool async = _tunnel.ReceiveAsync(args);
                if (async) break;
            }

            if (!isTunnelActive()) {
                if (isDbgpSessionActive()) {
                    shutdownDbgpSession();
                }
            }
        }

        private void commandReceived(object sender, SocketAsyncEventArgs args)
        {
            Debug.Print("message: {0}", Encoding.ASCII.GetString(args.Buffer, 0, args.BytesTransferred));

            if (!isTunnelActive()) {
                if (isDbgpSessionActive()) {
                    _session.Close();
                    _session = null;
                }
                return;
            }

            while (isTunnelActive() && isDbgpSessionActive()) {
                _dbgpProtocol.processCommand(args);
                if (!isDbgpSessionActive()) {
                    break;
                }

                bool async = _session.ReceiveAsync(args);
                if (async) break;
            }

            if (!isDbgpSessionActive() && isTunnelActive()) {
                _statusBuffer[0] = STATUS_DISCONNECTED;
                _tunnel.Send(_statusBuffer, 1, SocketFlags.None);
            }
        }

        protected Socket connectSocket(string addr, int port)
        {
            IPHostEntry hostEntry = null;

            hostEntry = Dns.GetHostEntry(addr);

            foreach (IPAddress ipAddress in hostEntry.AddressList) {
                IPEndPoint ipe = new IPEndPoint(ipAddress, port);
                Socket socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                socket.Connect(ipe);

                if (socket.Connected) {
                    return socket;
                }
            }

            return null;
        }
    }

    class TunnelProtocol
    {
        private bool _isReading = false;
        private int _bytesToRead;

        public delegate void ErrorStatusEventHandler(object sender, byte status);
        public delegate void MessageEventHandler(object sender, byte[] message, int offset, int length, SocketAsyncEventArgs args);
        public event ErrorStatusEventHandler onErrorStatus;
        public event MessageEventHandler onMessage;

        public void processMessage(SocketAsyncEventArgs args)
        {
            processMessage(args.Buffer, args.Offset, args.BytesTransferred, args);
        }

        private void processMessage(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            if (length <= 0) return;

            if (_isReading) {
                processFollowingMessage(buffer, offset, length, args);
            } else {
                processNewMessage(buffer, offset, length, args);
            }
        }

        private void processFollowingMessage(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            if (length < _bytesToRead) {
                _bytesToRead -= length;
                dispatchOnMessage(buffer, offset, length, args);
            } else {
                _isReading = false;
                int nextOffset = offset + _bytesToRead;
                int nextLength = length - _bytesToRead;
                dispatchOnMessage(buffer, offset, _bytesToRead, args);
                if (nextOffset < length) {
                    processMessage(buffer, nextOffset, nextLength, args);
                }
            }
        }

        private void processNewMessage(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            const int headerLength = 1;
            if (buffer[offset] != DbgpTunnel.STATUS_OK) {
                if (onErrorStatus != null) {
                    onErrorStatus(this, buffer[offset]);
                }
                return;
            }

            int messageOffset = offset + headerLength;
            int index = messageOffset;
            while (index < offset + length && buffer[index] != 0) {
                ++index;
            }

            string lengthString = Encoding.ASCII.GetString(buffer, messageOffset, index - messageOffset);
            int messageBodyOffset = index + 1;
            _bytesToRead = int.Parse(lengthString) + lengthString.Length + 2; // 2: length of termination char('\0') * 2

            if (length - headerLength == _bytesToRead) {
                dispatchOnMessage(buffer, messageOffset, length - headerLength, args);
                return;
            }

            if (length - headerLength < _bytesToRead) {
                _isReading = true;
                _bytesToRead -= length - headerLength;
                dispatchOnMessage(buffer, messageOffset, length - headerLength, args);
                return;
            }

            int nextOffset = messageOffset + _bytesToRead;
            int nextLength = length - _bytesToRead - headerLength;
            dispatchOnMessage(buffer, messageOffset, _bytesToRead, args);
            processNewMessage(buffer, nextOffset, nextLength, args);
        }

        private void dispatchOnMessage(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            if (onMessage != null) {
                onMessage(this, buffer, offset, length, args);
            }
        }
    }

    class DbgpProtocol
    {
        public delegate void CommandStartHandler(object sender);
        public delegate void CommandHandler(object sender, byte[] buffer, int offset, int length, SocketAsyncEventArgs args);

        public event CommandStartHandler onCommandStart;
        public event CommandHandler onCommand;

        private bool _isSending = false;

        public void processCommand(SocketAsyncEventArgs args)
        {
 	        processCommand(args.Buffer, args.Offset, args.BytesTransferred, args);
        }

        public void processCommand(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            if (!_isSending) {
                dispachCommandStart();
                _isSending = true;
            }

            processCommandInternal(buffer, offset, length, args);
        }

        private void processCommandInternal(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            for (int i = offset; i < length; ++i) {
                if (buffer[i] != 0) continue;

                dispachCommand(buffer, offset, i - offset + 1, args);
                int nextOffset = i + 1;
                if (nextOffset == length) {
                    _isSending = false;
                } else {
                    processCommand(buffer, nextOffset, length - nextOffset, args);
                }
                return;
            }
            dispachCommand(buffer, offset, length - offset, args);
        }

        private void dispachCommand(byte[] buffer, int offset, int length, SocketAsyncEventArgs args)
        {
            if (onCommand != null) {
                onCommand(this, buffer, offset, length, args);
            }
        }

        private void dispachCommandStart()
        {
            if (onCommandStart != null) {
                onCommandStart(this);
            }
        }    
    }
}
